"use client"; import { use, useEffect, useMemo, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { ArrowLeft, Save, AlertTriangle, Pencil, X, Copy, Check, BookMarked, Hash, Clock, FileText, } from "lucide-react"; import { PageHeader } from "@/components/common/page-header"; import { LearningHostProvenance } from "@/components/common/host-badge"; import { EmptyState } from "@/components/common/empty-state"; import { DeleteLearningDangerZone } from "@/components/common/delete-learning-danger-zone"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge"; import { Separator } from "@/components/ui/separator"; import { reflexio } from "@/lib/reflexio-client"; import { formatTimestamp, truncateId } from "@/lib/format"; import { useRequestHostAttribution } from "@/lib/host-attribution"; import { cn } from "@/lib/utils"; import { statusLabel } from "@/lib/status"; import type { StatusLabel } from "@/lib/status"; import type { UserPlaybook } from "@/lib/types"; type FormState = { content: string; trigger: string; rationale: string }; function toForm(p: UserPlaybook): FormState { return { content: p.content, trigger: p.trigger ?? "", rationale: p.rationale ?? "", }; } function displayName(name: string | null | undefined): string | null { if (!name) return null; if (name === "default_playbook_extractor") return "project-specific skill"; return name; } export default function ProjectSkillDetailPage({ params, }: { params: Promise<{ id: string }>; }) { const { id } = use(params); const router = useRouter(); const [playbook, setPlaybook] = useState(null); const [notFound, setNotFound] = useState(false); const [error, setError] = useState(null); const [saving, setSaving] = useState(false); const [deleting, setDeleting] = useState(false); const [editing, setEditing] = useState(false); const [form, setForm] = useState({ content: "", trigger: "", rationale: "", }); const attribution = useRequestHostAttribution(); useEffect(() => { let cancelled = false; reflexio .getUserPlaybooks({}) .then((res) => { if (cancelled) return; const found = (res.user_playbooks ?? []).find( (p) => String(p.user_playbook_id) === id, ); if (!found) { setNotFound(true); return; } setPlaybook(found); setForm(toForm(found)); }) .catch((e) => { if (!cancelled) setError(e instanceof Error ? e.message : String(e)); }); return () => { cancelled = true; }; }, [id]); const dirty = useMemo(() => { if (!playbook) return false; const orig = toForm(playbook); return ( orig.content !== form.content || orig.trigger !== form.trigger || orig.rationale !== form.rationale ); }, [playbook, form]); const save = async () => { if (!playbook || !dirty) return; setSaving(true); setError(null); try { await reflexio.updateUserPlaybook( { user_playbook_id: playbook.user_playbook_id, content: form.content, trigger: form.trigger || null, rationale: form.rationale || null, }, ); setPlaybook({ ...playbook, content: form.content, trigger: form.trigger || null, rationale: form.rationale || null, }); setEditing(false); } catch (e) { setError(e instanceof Error ? e.message : String(e)); } finally { setSaving(false); } }; const remove = async () => { if (!playbook) return; setDeleting(true); try { await reflexio.deleteUserPlaybook(playbook.user_playbook_id); router.push("/skills"); } catch (e) { setError(e instanceof Error ? e.message : String(e)); setDeleting(false); } }; const cancelEdit = () => { if (playbook) setForm(toForm(playbook)); setEditing(false); }; if (notFound) { return (
} />
); } const status = playbook ? statusLabel(playbook) : null; return (
{!editing ? ( ) : ( <> )}
} />
{error && (
{error}
)} {playbook && (
{displayName(playbook.playbook_name) && ( {displayName(playbook.playbook_name)} )} {dirty && ( unsaved changes )}
)}
{editing ? ( setForm((f) => ({ ...f, trigger: v }))} rows={2} placeholder="e.g. When writing or running async Python tests." /> ) : ( )}
{editing ? ( setForm((f) => ({ ...f, content: v }))} rows={6} placeholder="e.g. Use anyio with trio backend — never pytest-asyncio." /> ) : ( )}
{editing ? ( setForm((f) => ({ ...f, rationale: v }))} rows={3} placeholder="e.g. pytest-asyncio deadlocked CI on project X — trio is the project standard." /> ) : ( )}
{!editing && playbook && ( <> )}
{playbook && ( )}
); } function Section({ icon: Icon, title, hint, children, }: { icon: React.ComponentType<{ className?: string }>; title: string; hint?: string; children: React.ReactNode; }) { return (
{hint && ( {hint} )}
{children}
); } function Prose({ text, muted = false }: { text: string; muted?: boolean }) { if (!text) { return (

{muted ? "Not set" : "—"}

); } return (

{text}

); } function AutoTextarea({ value, onChange, rows = 3, placeholder, }: { value: string; onChange: (v: string) => void; rows?: number; placeholder?: string; }) { return (